//   Copyright 2012,2013 Vaughn Vernon
//
//   Licensed under the Apache License, Version 2.0 (the "License");
//   you may not use this file except in compliance with the License.
//   You may obtain a copy of the License at
//
//       http://www.apache.org/licenses/LICENSE-2.0
//
//   Unless required by applicable law or agreed to in writing, software
//   distributed under the License is distributed on an "AS IS" BASIS,
//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//   See the License for the specific language governing permissions and
//   limitations under the License.

package com.saasovation.collaboration.port.adapter.event;

import com.saasovation.common.domain.model.DomainEventPublisher;
import com.saasovation.common.event.sourcing.DispatchableDomainEvent;
import com.saasovation.common.event.sourcing.EventDispatcher;
import com.saasovation.common.event.sourcing.EventNotifiable;
import com.saasovation.common.event.sourcing.EventStore;
import jakarta.annotation.PostConstruct;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;

import java.util.*;

public class FollowStoreEventDispatcher implements EventDispatcher, EventNotifiable {

    private long lastDispatchedEventId;
    private List<EventDispatcher> registeredDispatchers;
    private EventStore eventStore;
    private final NamedParameterJdbcTemplate jdbcTemplate;

    public FollowStoreEventDispatcher(EventStore eventStore,
                                      NamedParameterJdbcTemplate jdbcTemplate) {
        super();
        this.setRegisteredDispatchers(new ArrayList<>());

        this.setEventStore(eventStore);
        eventStore.registerEventNotifiable(this);

        this.jdbcTemplate = jdbcTemplate;
    }

    @PostConstruct
    public void init() {
        this.setLastDispatchedEventId(this.queryLastDispatchedEventId());
        this.notifyDispatchableEvents();
    }

    @Override
    public void dispatch(DispatchableDomainEvent aDispatchableDomainEvent) {
        DomainEventPublisher.instance().publish(aDispatchableDomainEvent.domainEvent());
        for (EventDispatcher eventDispatcher : this.registeredDispatchers()) {
            eventDispatcher.dispatch(aDispatchableDomainEvent);
        }
    }

    @Override
    public void notifyDispatchableEvents() {
        // this could be multi-threaded from here, but is not for simplicity;
        // child EventDispatchers should use only ConnectionProvider.connection() and
        // not commit. i will commit and close the connection here;
        List<DispatchableDomainEvent> unDispatchedEvents =
                this.eventStore().eventsSince(this.lastDispatchedEventId());
        if (!unDispatchedEvents.isEmpty()) {
            for (DispatchableDomainEvent event : unDispatchedEvents) {
                this.dispatch(event);
            }
            DispatchableDomainEvent withLastEventId = unDispatchedEvents.getLast();
            long lastDispatchedEventId = withLastEventId.eventId();
            this.setLastDispatchedEventId(lastDispatchedEventId);
            this.saveLastDispatchedEventId(lastDispatchedEventId);
        }
    }

    @Override
    public void registerEventDispatcher(EventDispatcher anEventDispatcher) {
        this.registeredDispatchers().add(anEventDispatcher);
    }

    @Override
    public boolean understands(DispatchableDomainEvent aDispatchableDomainEvent) {
        return true;
    }

    private long lastDispatchedEventId() {
        return this.lastDispatchedEventId;
    }

    private void setLastDispatchedEventId(long aLastDispatchedEventId) {
        this.lastDispatchedEventId = aLastDispatchedEventId;
    }

    private long queryLastDispatchedEventId() {
        long defaultEventId = 0L;
        String sql = "select max(event_id) from tbl_dispatcher_last_event";
        Long result = null;
        try {
            result = this.jdbcTemplate.queryForObject(sql, new HashMap<>(), Long.class);
        } catch (EmptyResultDataAccessException e) {
            // ignore
        }
        long lastHandledEventId = Optional.ofNullable(result).orElse(defaultEventId);
        if (lastHandledEventId == defaultEventId) {
            this.saveLastDispatchedEventId(defaultEventId);
        }
        return lastHandledEventId;
    }

    private void saveLastDispatchedEventId(long aLastDispatchedEventId) {
        String updateSql = "update tbl_dispatcher_last_event set event_id = :eventId";
        Map<String, Object> params = new HashMap<>();
        params.put("eventId", aLastDispatchedEventId);
        int updated = this.jdbcTemplate.update(updateSql, params);

        if (updated == 0) {
            String insertSql = "insert into tbl_dispatcher_last_event(event_id) values(:eventId)";
            this.jdbcTemplate.update(insertSql, params);
        }
    }

    private List<EventDispatcher> registeredDispatchers() {
        return this.registeredDispatchers;
    }

    private void setRegisteredDispatchers(List<EventDispatcher> aDispatchers) {
        this.registeredDispatchers = aDispatchers;
    }

    private void setEventStore(EventStore eventStore) {
        this.eventStore = eventStore;
    }

    private EventStore eventStore() {
        return this.eventStore;
    }
}
